using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using HooIzDat.Web.Helpers;

namespace HooIzDat.Web.Models
{
    public partial class Game
    {
        public const int PendingGameTimeoutMinutes = 10;

        public bool HasStarted { get { return StartedOn.HasValue; } }
        public bool IsFinished { get { return FinishedOn.HasValue; } }
        public bool IsPending { get { return Player2 == null; } }

        public bool IsExpired
        {
            get { return (ExpiresIn ?? 1) <= 0; }
        }

        public double? ExpiresIn
        {
            get
            {
                return !IsPending ? new Nullable<double>() :
                  PendingGameTimeoutMinutes - (DateTime.Now - CreatedOn).TotalMinutes;
            }
        }

        public bool IsWaitingForQuestion
        {
            get
            {
                return HasStarted && !IsFinished &&
                  GameState != null && String.IsNullOrEmpty(GameState.QuestionAsked);
            }
        }

        public bool IsWaitingForAnswer
        {
            get
            {
                return HasStarted && !IsFinished &&
                  GameState != null && !String.IsNullOrEmpty(GameState.QuestionAsked) &&
                  !GameState.Answer.HasValue;
            }
        }

        public bool IsWaitingForNextTurn
        {
            get
            {
                return HasStarted && !IsFinished &&
                  GameState != null &&
                  (!String.IsNullOrEmpty(GameState.QuestionAsked) || GameState.GuessedCharacterID != null) &&
                  GameState.Answer.HasValue;
            }
        }

        public bool HasPlayer(Player player)
        {
            return Player1 == player || Player2 == player;
        }

        public bool HasPlayer(string userName)
        {
            return Player1.UserName.Equals(userName, StringComparison.InvariantCultureIgnoreCase) ||
                   Player2.UserName.Equals(userName, StringComparison.InvariantCultureIgnoreCase);
        }

        public void Join(Player player2, IEnumerable<int> allCharacterIds)
        {
            if (Player2 != null)
                throw new InvalidOperationException("This game already has 2 players.");

            if (allCharacterIds.Count() < 12)
                throw new InvalidOperationException("There are not enough characters defined in the system to play a game. A minimum of 12 characters must be defined.");

            Player2 = player2;
            StartedOn = DateTime.Now;
            GameState = new GameState
            {
                Game = this,
                ActivePlayerID = new[] { Player1ID, Player2.PlayerID }.TakeRandom(1).Single()
            };
            AssignPeople(this, allCharacterIds);
            AssignTiles(this, allCharacterIds);
        }

        private static void AssignPeople(Game game, IEnumerable<int> allCharacterIds)
        {
            game.Player1CharacterID = allCharacterIds.First();
            game.Player2CharacterID = allCharacterIds.Except(new[] { game.Player1CharacterID.Value }).First();
        }

        private static void AssignTiles(Game game, IEnumerable<int> allCharacterIds)
        {
            if (game.Player1CharacterID == null || game.Player2CharacterID == null)
                throw new InvalidOperationException("Players must have people cards assigned before assigning tiles.");

            // Need to assign 12 tiles to each player
            // Each player can have different characters on their tiles but must have
            // the character assigned to the other player of course
            var player1TileCharacterIds = allCharacterIds
                .Where(p => p != game.Player2CharacterID.Value)
                .Take(11)
                .Concat(new[] { game.Player2CharacterID.Value }); // Add player 2 character back in

            var player2TileCharacterIds = allCharacterIds
                .Where(p => p != game.Player1CharacterID.Value)
                .Take(11)
                .Concat(new[] { game.Player1CharacterID.Value }); // Add player 1 character back in

            var player1TileIndex = 1;
            var player2TileIndex = 1;
            var tiles = player1TileCharacterIds
                .Select(p => new { Player = game.Player1, CharacterID = p, Index = player1TileIndex++ })
                .Concat(player2TileCharacterIds
                    .Select(p => new { Player = game.Player2, CharacterID = p, Index = player2TileIndex++ }))
                .Select(t => new GameTile
                {
                    IsOpen = true,
                    CharacterID = t.CharacterID,
                    Player = t.Player,
                    Game = game,
                    TileIndex = t.Index
                })
                .ToList();

            Debug.Assert(tiles.Count == 24); // There should be 12 tiles each

            foreach (var tile in tiles)
            {
                game.Tiles.Add(tile);
            }
        }

        public void FinishCurrentTurn()
        {
            FinishCurrentTurn(false);
        }

        public void FinishCurrentTurn(bool failedGuess)
        {
            // Check state
            if (!failedGuess && !IsWaitingForNextTurn)
                throw new InvalidOperationException("Game is not ready to finish current turn.");

            GameState.ActivePlayer = (Player1 == GameState.ActivePlayer ? Player2 : Player1);
            GameState.QuestionAsked = "";
            GameState.Answer = null;
            GameState.GuessedCharacterID = null;
            GameState.TurnsPlayed++;
        }

        public void Won()
        {
            // Check state
            if (!IsWaitingForQuestion)
                throw new InvalidOperationException("Game can only be won when waiting for a question.");

            Winner = GameState.ActivePlayer;
            FinishedOn = DateTime.Now;
            QuestionsWonIn = GameState.TurnsPlayed;
            GameState.Answer = true;
        }

        public virtual int GameID
        {
            get;
            set;
        }
    
        public virtual Nullable<System.DateTime> StartedOn
        {
            get;
            set;
        }
    
        public virtual Nullable<System.DateTime> FinishedOn
        {
            get;
            set;
        }
    
        public virtual int Player1ID
        {
            get { return _player1ID; }
            set
            {
                try
                {
                    _settingFK = true;
                    if (_player1ID != value)
                    {
                        if (Player1 != null && Player1.PlayerID != value)
                        {
                            Player1 = null;
                        }
                        _player1ID = value;
                    }
                }
                finally
                {
                    _settingFK = false;
                }
            }
        }
        private int _player1ID;
    
        public virtual Nullable<int> Player2ID
        {
            get { return _player2ID; }
            set
            {
                try
                {
                    _settingFK = true;
                    if (_player2ID != value)
                    {
                        if (Player2 != null && Player2.PlayerID != value)
                        {
                            Player2 = null;
                        }
                        _player2ID = value;
                    }
                }
                finally
                {
                    _settingFK = false;
                }
            }
        }
        private Nullable<int> _player2ID;
    
        public virtual Nullable<int> Player1CharacterID
        {
            get { return _player1CharacterID; }
            set
            {
                try
                {
                    _settingFK = true;
                    if (_player1CharacterID != value)
                    {
                        if (Player1Character != null && Player1Character.CharacterID != value)
                        {
                            Player1Character = null;
                        }
                        _player1CharacterID = value;
                    }
                }
                finally
                {
                    _settingFK = false;
                }
            }
        }
        private Nullable<int> _player1CharacterID;
    
        public virtual Nullable<int> Player2CharacterID
        {
            get { return _player2CharacterID; }
            set
            {
                try
                {
                    _settingFK = true;
                    if (_player2CharacterID != value)
                    {
                        if (Player2Character != null && Player2Character.CharacterID != value)
                        {
                            Player2Character = null;
                        }
                        _player2CharacterID = value;
                    }
                }
                finally
                {
                    _settingFK = false;
                }
            }
        }
        private Nullable<int> _player2CharacterID;
    
        public virtual Nullable<int> WinnerID
        {
            get { return _winnerID; }
            set
            {
                try
                {
                    _settingFK = true;
                    if (_winnerID != value)
                    {
                        if (Winner != null && Winner.PlayerID != value)
                        {
                            Winner = null;
                        }
                        _winnerID = value;
                    }
                }
                finally
                {
                    _settingFK = false;
                }
            }
        }
        private Nullable<int> _winnerID;
    
        public virtual Nullable<int> QuestionsWonIn
        {
            get;
            set;
        }
    
        public virtual System.DateTime CreatedOn
        {
            get;
            set;
        }

        public virtual Character Player1Character
        {
            get { return _player1Character; }
            set
            {
                if (!ReferenceEquals(_player1Character, value))
                {
                    var previousValue = _player1Character;
                    _player1Character = value;
                    FixupPlayer1Character(previousValue);
                }
            }
        }
        private Character _player1Character;
    
        public virtual Character Player2Character
        {
            get { return _player2Character; }
            set
            {
                if (!ReferenceEquals(_player2Character, value))
                {
                    var previousValue = _player2Character;
                    _player2Character = value;
                    FixupPlayer2Character(previousValue);
                }
            }
        }
        private Character _player2Character;
    
        public virtual Player Player1
        {
            get { return _player1; }
            set
            {
                if (!ReferenceEquals(_player1, value))
                {
                    var previousValue = _player1;
                    _player1 = value;
                    FixupPlayer1(previousValue);
                }
            }
        }
        private Player _player1;
    
        public virtual Player Player2
        {
            get { return _player2; }
            set
            {
                if (!ReferenceEquals(_player2, value))
                {
                    var previousValue = _player2;
                    _player2 = value;
                    FixupPlayer2(previousValue);
                }
            }
        }
        private Player _player2;
    
        public virtual Player Winner
        {
            get { return _winner; }
            set
            {
                if (!ReferenceEquals(_winner, value))
                {
                    var previousValue = _winner;
                    _winner = value;
                    FixupWinner(previousValue);
                }
            }
        }
        private Player _winner;
    
        public virtual GameState GameState
        {
            get { return _gameState; }
            set
            {
                if (!ReferenceEquals(_gameState, value))
                {
                    var previousValue = _gameState;
                    _gameState = value;
                    FixupGameState(previousValue);
                }
            }
        }
        private GameState _gameState;
    
        public virtual ICollection<GameTile> Tiles
        {
            get
            {
                if (_tiles == null)
                {
                    var newCollection = new FixupCollection<GameTile>();
                    newCollection.CollectionChanged += FixupTiles;
                    _tiles = newCollection;
                }
                return _tiles;
            }
            set
            {
                if (!ReferenceEquals(_tiles, value))
                {
                    var previousValue = _tiles as FixupCollection<GameTile>;
                    if (previousValue != null)
                    {
                        previousValue.CollectionChanged -= FixupTiles;
                    }
                    _tiles = value;
                    var newValue = value as FixupCollection<GameTile>;
                    if (newValue != null)
                    {
                        newValue.CollectionChanged += FixupTiles;
                    }
                }
            }
        }
        private ICollection<GameTile> _tiles;

        private bool _settingFK = false;
    
        private void FixupPlayer1Character(Character previousValue)
        {
            if (previousValue != null && previousValue.Games.Contains(this))
            {
                previousValue.Games.Remove(this);
            }
    
            if (Player1Character != null)
            {
                if (!Player1Character.Games.Contains(this))
                {
                    Player1Character.Games.Add(this);
                }
                if (Player1CharacterID != Player1Character.CharacterID)
                {
                    Player1CharacterID = Player1Character.CharacterID;
                }
            }
            else if (!_settingFK)
            {
                Player1CharacterID = null;
            }
        }
    
        private void FixupPlayer2Character(Character previousValue)
        {
            if (previousValue != null && previousValue.Games1.Contains(this))
            {
                previousValue.Games1.Remove(this);
            }
    
            if (Player2Character != null)
            {
                if (!Player2Character.Games1.Contains(this))
                {
                    Player2Character.Games1.Add(this);
                }
                if (Player2CharacterID != Player2Character.CharacterID)
                {
                    Player2CharacterID = Player2Character.CharacterID;
                }
            }
            else if (!_settingFK)
            {
                Player2CharacterID = null;
            }
        }
    
        private void FixupPlayer1(Player previousValue)
        {
            if (previousValue != null && previousValue.Games.Contains(this))
            {
                previousValue.Games.Remove(this);
            }
    
            if (Player1 != null)
            {
                if (!Player1.Games.Contains(this))
                {
                    Player1.Games.Add(this);
                }
                if (Player1ID != Player1.PlayerID)
                {
                    Player1ID = Player1.PlayerID;
                }
            }
        }
    
        private void FixupPlayer2(Player previousValue)
        {
            if (previousValue != null && previousValue.Games1.Contains(this))
            {
                previousValue.Games1.Remove(this);
            }
    
            if (Player2 != null)
            {
                if (!Player2.Games1.Contains(this))
                {
                    Player2.Games1.Add(this);
                }
                if (Player2ID != Player2.PlayerID)
                {
                    Player2ID = Player2.PlayerID;
                }
            }
            else if (!_settingFK)
            {
                Player2ID = null;
            }
        }
    
        private void FixupWinner(Player previousValue)
        {
            if (previousValue != null && previousValue.Games2.Contains(this))
            {
                previousValue.Games2.Remove(this);
            }
    
            if (Winner != null)
            {
                if (!Winner.Games2.Contains(this))
                {
                    Winner.Games2.Add(this);
                }
                if (WinnerID != Winner.PlayerID)
                {
                    WinnerID = Winner.PlayerID;
                }
            }
            else if (!_settingFK)
            {
                WinnerID = null;
            }
        }
    
        private void FixupGameState(GameState previousValue)
        {
            if (previousValue != null && ReferenceEquals(previousValue.Game, this))
            {
                previousValue.Game = null;
            }
    
            if (GameState != null)
            {
                GameState.Game = this;
            }
        }
    
        private void FixupTiles(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (GameTile item in e.NewItems)
                {
                    item.Game = this;
                }
            }
    
            if (e.OldItems != null)
            {
                foreach (GameTile item in e.OldItems)
                {
                    if (ReferenceEquals(item.Game, this))
                    {
                        item.Game = null;
                    }
                }
            }
        }
    }
}
